package com.baomidou.mybatisplus.core.override

import com.wh1200.jackson.createObjectMapper
import com.wh1200.mybatis.xmlless.annotations.JsonMappingProperty
import com.wh1200.mybatis.xmlless.annotations.JsonResult
import com.wh1200.mybatis.xmlless.methods.XmlLessMethods
import com.wh1200.mybatis.xmlless.model.JsonWrapper
import com.wh1200.mybatis.xmlless.model.TotalPageRequest
import com.wh1200.mybatis.xmlless.model.exception.XmlLessException
import com.wh1200.mybatis.xmlless.resolver.TypeResolver
import com.wh1200.mybatis.xmlless.util.XmllessInfoUtil
import com.wh1200.mybatis.xmlless.util.checkType
import com.baomidou.mybatisplus.core.metadata.IPage
import org.apache.ibatis.binding.MapperMethod
import org.apache.ibatis.mapping.SqlCommandType
import org.apache.ibatis.session.Configuration
import org.apache.ibatis.session.SqlSession
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import java.lang.reflect.Method

/**
 *
 * Created by 吴昊 on 2018/12/13.
 *
 * @author 吴昊
 * @since 0.0.4
 */
internal class XmlLessPageMapperMethod(
    private val mapperInterface: Class<*>,
    private val requestMethod: Method,
    config: Configuration
) : MapperMethod(mapperInterface, requestMethod, config) {

  private val objectMapper = createObjectMapper()
  private val command = SqlCommand(config, mapperInterface, requestMethod)
  private val method = MethodSignature(config, mapperInterface, requestMethod)

  @Suppress("UNCHECKED_CAST")
  override fun execute(sqlSession: SqlSession, args: Array<Any?>?): Any? {
    var result: Any? = null
    if (command.type == SqlCommandType.SELECT && args != null) {
      result = processSelect(args, sqlSession)
    }
    if (result == null) {
      try {
        result = super.execute(sqlSession, args)
      } catch (e: Exception) {
        e.printStackTrace()
        throw e
      }
    }
    if (result == null) {
      return null
    }
    val returnClass = TypeResolver.resolveNullableReturnMappingType(requestMethod, mapperInterface)
    val forceSingleValue = forceSingleValue(requestMethod)
    val type = TypeResolver.resolveReturnJavaType(requestMethod, mapperInterface, forceSingleValue)
    if (requestMethod.isAnnotationPresent(JsonResult::class.java)
        || (returnClass != null && returnClass.isAnnotationPresent(JsonMappingProperty::class.java))
    ) {
      if (forceSingleValue) {
        val size = (result as MutableCollection<Any?>).size
        return if (size > 1) {
          throw XmlLessException("方法应该返回【1】个结果, 但返回了【$size】个")
        } else if (size == 0) {
          null
        } else {
          val value = result.first()
          when (val json = extractJson(value)) {
            null -> null
            else -> objectMapper.readValue<Any?>(json, type)
          }
        }
      }
      if (result is MutableCollection<*>) {
        val list = arrayListOf<Any?>()
        list.addAll(result as Collection<Any?>)
        result.clear()
        (result as MutableCollection<Any?>).addAll(
            list.mapNotNull {
              when (val json = extractJson(it)) {
                null -> null
                else -> objectMapper.readValue<Any?>(json, type)
              }
            }
        )
      } else {
        val json = (result as JsonWrapper).json ?: return null
        result = objectMapper.readValue<Any?>(json, type)
      }
    }
    return result
  }

  private fun processSelect(
      args: Array<Any?>,
      sqlSession: SqlSession,
  ): Any? {
    if (Page::class.java.isAssignableFrom(method.returnType)) {
      val pageableIndex = args.indexOfFirst { it is Pageable }
      if (pageableIndex >= 0) {
        val pageable = args[pageableIndex] as Pageable
        if (!pageable.isUnpaged) {
          args[pageableIndex] = TotalPageRequest(pageable.pageNumber, pageable.pageSize, pageable.sort)
        }
      }
      val list = executeForMany<Any>(sqlSession, args)
      val pageArg = findIPageArg(args) as IPage<*>?
      return if (pageArg != null) {
        PageImpl(list, PageRequest.of((pageArg.current - 1).toInt(), pageArg.size.toInt()), pageArg.total)
      } else {
        val pageableArg = args.firstOrNull { it is Pageable } as Pageable?
        if (XmllessInfoUtil.isXmllessResolvedMethod(requestMethod)) {
          val total = executeForTotal(sqlSession, args)
          if (pageableArg != null) {
            PageImpl(list, pageableArg, total)
          } else {
            PageImpl(list, PageRequest.of(0, list.size), total)
          }
        } else {
          val pageable = args.find { it is TotalPageRequest } as TotalPageRequest?
          PageImpl(list, PageRequest.of(0, pageable?.pageSize ?: 1), pageable?.getTotal() ?: 0)
        }
      }
    } else if (IPage::class.java.isAssignableFrom(method.returnType)) {
      val list = executeForMany<Any>(sqlSession, args)
      val pageArg = findIPageArg(args)
      return if (pageArg != null) {
        pageArg.records = list
        pageArg
      } else {
        null
      }
    }
    return null
  }

  private fun extractJson(it: Any?): String? {
    return when (it) {
      null           -> null
      is String      -> it
      is JsonWrapper -> it.json
      else           -> it.toString()
    }
  }

  private fun <E> executeForMany(sqlSession: SqlSession, args: Array<out Any?>?): List<E> {
    val param = method.convertArgsToSqlCommandParam(args)
    return if (method.hasRowBounds()) {
      val rowBounds = method.extractRowBounds(args)
      sqlSession.selectList(command.name, param, rowBounds)
    } else {
      sqlSession.selectList(command.name, param)
    }
  }

  private fun executeForTotal(sqlSession: SqlSession, args: Array<out Any?>?): Long {
    val param = method.convertArgsToSqlCommandParam(args)
    val commandName = command.name + XmlLessMethods.COUNT_STATEMENT_SUFFIX
    return sqlSession.selectOne(commandName, param) ?: 0L
  }

  private fun findIPageArg(args: Array<out Any?>): IPage<Any>? {
    val arg = args.filterNotNull().firstOrNull {
      IPage::class.java.isAssignableFrom(it.javaClass)
    } ?: return null
    arg.checkType<IPage<Any>>()
    return arg
  }

  private fun forceSingleValue(requestMethod: Method): Boolean {
    return requestMethod.isAnnotationPresent(JsonResult::class.java)
        && requestMethod.getAnnotation(JsonResult::class.java).forceSingleValue
  }

}
